﻿
namespace TingSLsocket

open System
open System.Net
open System.Net.Sockets
open SocketExtend

[<Sealed>]
type TcpConnector(m_socket :Socket,svrEndpnt : EndPoint) =

    let m_evtEstablished = new Event<unit>()
    let m_evtFail = new Event<SocketError>()
    let m_connectSaea = new SocketAsyncEventArgs(RemoteEndPoint = svrEndpnt)

    let rec handleConnection (saea : SocketAsyncEventArgs) =
        match saea.LastOperation with
        | SocketAsyncOperation.Connect -> 
            processConnect saea
        | _ ->
            failwith "only support Connect and Disconnect"

    and processConnect saea =
        match saea.SocketError with
        | SocketError.Success ->
            m_evtEstablished.Trigger ()
        | _ ->
            m_evtFail.Trigger saea.SocketError

    do
        m_connectSaea.Completed.Add handleConnection

    interface IDisposable with
        member this.Dispose() =
            m_connectSaea.Dispose()

    [<CLIEvent>]
    member this.EstablishedEvt = m_evtEstablished.Publish
    [<CLIEvent>]
    member this.FailedEvt = m_evtFail.Publish

    member this.ConnectAsync() =
        m_socket.ConnectAsyncSafe m_connectSaea handleConnection
    

[<Sealed>]
type TcpClient(svrEndpnt : EndPoint,bufSize) =
    // ***************************** primary constructor
    let m_evtConnected = new Event<unit>()
    let m_evtClosed = new Event<SocketError>()
    let m_evtDataRecvExpose = new Event<byte[]*int*int>()

    let m_socket = NewTcpSocket()
    let m_connector = new TcpConnector(m_socket,svrEndpnt)

    [<VolatileField>]
    let mutable m_channel : TcpRemoteChannel = null

    let close error =
        if m_channel <> null then
            m_channel.Close ChannelCloseFlag.All
            m_channel <- null
        
        (m_connector :> IDisposable).Dispose()

        m_evtClosed.Trigger error

    let rec onIoCompleted (saea : SocketAsyncEventArgs) =
        match saea.LastOperation with
        | SocketAsyncOperation.Receive -> processReceived saea
        | SocketAsyncOperation.Send -> processSent saea
        | _ -> failwith "only support Receive and Send"

    and processReceived (saea : SocketAsyncEventArgs) =
        let channel = saea.UserToken :?> TcpRemoteChannel
        
        if saea.SocketError = SocketError.Success && saea.BytesTransferred > 0 then
            m_evtDataRecvExpose.Trigger(saea.Buffer,saea.Offset,saea.BytesTransferred)
            channel.ReceiveAsync()
        else
            close saea.SocketError

    and processSent saea =
        let channel = saea.UserToken :?> TcpRemoteChannel

        match saea.SocketError with
        | SocketError.Success -> 
            channel.HandleSent saea.BytesTransferred
        | _ ->
            close saea.SocketError

    let onConnected() =
        // create new SAEA, the old ones have been disposed before disconnecting
        let channelArgs = 
            ChannelEvtArgs.Make bufSize onIoCompleted 1
            |> Seq.head

        // because we create a new channel, the previous sending queue is dropped
        // no impact from last connection
        m_channel <- new TcpRemoteChannel(Guid.Empty, m_socket,channelArgs)

        m_evtConnected.Trigger()
        m_channel.ReceiveAsync()

        // release the SAEA for connecting as early as possible
        (m_connector :> IDisposable).Dispose()

    do
        m_connector.EstablishedEvt.Add onConnected
        m_connector.FailedEvt.Add close

    // ***************************** destructor
    interface IDisposable with
        member this.Dispose() =
            close SocketError.Success // active close

    // ***************************** public API
    [<CLIEvent>]
    member this.ConnectedEvt = m_evtConnected.Publish
    
    // note: naming it "Expose" to remind the caller of this Event
    // that it expose the internal data without copy
    // so the caller should use it carefully
    // the data passed by this event cannot be modified
    [<CLIEvent>]
    member this.DataRecvExposeEvt = m_evtDataRecvExpose.Publish
    
    [<CLIEvent>]
    member this.ClosedEvt = m_evtClosed.Publish

    member this.ConnectAsync() =
        m_connector.ConnectAsync()

    member this.SendAsync(data,offset,size) =
        if m_channel = null then
            raise <| new InvalidOperationException("socket not connected yet")
        else
            m_channel.SendAsync(data,offset,size)
        
            
            
